from pkgr.database import Database
from pkgr.rpm.rpmpackage import RPMInstalledPackage
import logging
import rpm
import os
import sys
import tempfile

logger = logging.getLogger('pkgr.rpm.database')

# Global file descriptor for the callback.
rpmtsCallback_fd = None

class RPMDatabase(Database):
    """ Represents a rpm database of installed packages.
    """
    _dbpath = None
    _root = None
    
    LOG_LEVELS = {
    'emergency':   rpm.RPMLOG_EMERG,
    'alert':   rpm.RPMLOG_ALERT,
    'critical':    rpm.RPMLOG_CRIT,
    'error':     rpm.RPMLOG_ERR,
    'warning': rpm.RPMLOG_WARNING,
    'notice':  rpm.RPMLOG_NOTICE,
    'info':    rpm.RPMLOG_INFO,
    'debug':   rpm.RPMLOG_DEBUG
    }   

    @classmethod    
    def readonly(cls, root='/', dbpath=None):
        read_ts = cls(root=root, dbpath=dbpath)
        read_ts.set_vs_flags(rpm._RPMVSF_NOSIGNATURES|rpm._RPMVSF_NODIGESTS)
        return read_ts
        
    def __init__(self, root="/", dbpath=None):
        
        self.set_root(root)
        self.set_dbpath(dbpath)
        #self.enable_logging()
        self.open()
        self.enable_logging()
    
    _progress_callback = None
    def set_progress_callback(self, cb=None):
        self._progress_callback = cb
        
    def call_progress_callback(self, state, amount, total, pkg_filename, client_data):
        if self._progress_callback is None:
            return
            
        return self._progress_callback(state, pkg_filename[0], pkg_filename[1], amount, total)
        
    def __del__(self):
        self.close()
       
    def set_dbpath(self, path):
        self._dbpath = path
        
    def get_dbpath(self):
        return self._dbpath
        
    def set_root(self, root):
        self._root = root
        
    def get_root(self):
        return self._root
        
    def set_vs_flags(self, flags):
        """ Set verification flags.          
        
            Starting with rpm 4.1, package files are verified automatically,
            which can cause problems, especially if you are working with older
            packages, or packages without proper digital signatures.
            
            In most cases, the automatic verification is an advantage, since you
            can have greater confidence in the package files. However, you can
            call setVSFlags on a transaction set to change the default behavior.
            
            Table 17-3 Flags for setVSFlags:
            Flag                        Meaning
            ----                        -------
            rpm.RPMVSF_NEEDPAYLOAD      Leave the file handle positions at the beginning of the payload.
            rpm.RPMVSF_NOHDRCHK         Don't check the RPM database header.
            rpm.RPMVSF_ NODSA           Don't check the header and payload DSA signatures.
            rpm.RPMVSF_ NODSAHEADER     Don't check the header DSA signature.
            rpm.RPMVSF_ NOMD5           Don't check the header and payload MD5 digests.
            rpm.RPMVSF_ NORSA           Don't check the header and payload RSA signatures.
            rpm.RPMVSF_ NOSHA1HEADER    Don't check the header SHA1 digest.
            rpm._RPMVSF_NODIGESTS       Convenience to not check digests.
            rpm._RPMVSF_NOSIGNATURES    Convenience to not check signatures.
                                        To turn off all checks, you can pas 1 to setVSFlags:
                                        
            ts.setVSFlasgs(-1)
        """
        self.get_ts().setVSFlags(flags)
     
    def add_flags(self, flag):
        curflags = self.get_ts().setFlags(0)
        self.get_ts().setFlags(curflags | flag)
    
    def remove_flag(self, flag):
        curflags = self.get_ts().setFlags(0)
        self.get_ts().setFlags(curflags & ~flag)
        
    def get_flags(self):
        curflags = self.get_ts().setFlags(0)
        self.get_ts().setFlags(curflags)
        return curflags
        
    def is_flag_set(self, flag):
        val = self.get_flags()
        return bool(flag & val)
        
    
    def toggle_flag(self, flag, enabled):
        flag_set = self.is_flag_set(flag)
        if enabled and not flag_set:
            self.add_flags(flag)
        elif not enabled and flag_set:
            self.remove_flag(flag)
            
            
    def enable_allfiles(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_ALLFILES, b)
        
    def enable_justdb(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_JUSTDB, b)
        
    def enable_noscripts(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_NOSCRIPTS, b)
        
    def enable_nodocs(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_NODOCS, b)
        
    def enable_noconfigs(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_NOCONFIGS, b)
        
    def enable_nomd5(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_NOMD5, b)
        
    def enable_notriggers(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_NOTRIGGERS, b)
        
    def enable_repackage(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_REPACKAGE, b)
        
    def enable_test(self, b):
        self.toggle_flag(rpm.RPMTRANS_FLAG_TEST, b)
        
    def open(self):
        # set the dbpath and delete it after opening the transactionset
        # so that the following opens use the default
        dbpath = self.get_dbpath()
        if dbpath is None:
            dbpath = '/'
        rpm.addMacro("_dbpath", dbpath)
            
        logger.info('Opening database at %s using root %s', dbpath, self.get_root())
        
        self.ts = rpm.TransactionSet(self.get_root())
        self.is_open = True
        
        if dbpath is not None:
            rpm.delMacro("_dbpath")
     
    is_open = False
    ts = None
    
    @classmethod
    def load_header_from_package(cls, package):
        rots = cls.readonly()
        
        try:
            fdno = os.open(package, os.O_RDONLY)
        except OSError, e:
            raise Exception('Unable to open file')
    
        # XXX: We should start a readonly ts here, so we don't get the options
        # from the other one (sig checking, etc)
        try:
            hdr = rots.get_ts().hdrFromFdno(fdno)
        except rpm.error, e:
            os.close(fdno)
            raise Exception("RPM Error opening Package")
            
        if type(hdr) != rpm.hdr:
            os.close(fdno)
            raise Exception("RPM Error opening Package (type)")
    
        os.close(fdno)
        return hdr
        
    def close(self):
        if self.is_open:
            self.get_ts().closeDB()
            self.ts = None
            self.is_open = False
      
    def get_ts(self):
        if self.ts is None:
            raise Exception("TransactionSet not opened yet. Please call RPMDatabase.open().")
        return self.ts
        
    def add_install(self, pkg, filename):
        h = self.get_rpm_header(filename)
        self.get_ts().addInstall(h, (pkg, filename), 'i')
    
    def add_update(self, pkg, filename):
        h = self.get_rpm_header(filename)
        self.get_ts().addInstall(h, (pkg, filename), 'u')
       
    def add_erase(self, package_name):
        self.get_ts().addErase(package_name)
        
    def get_installed_providing(self, requirement):
        """ Returns true if the requirement is installed. """
        packages_providing = set()
        for package in self.find_packages_providing(requirement):
            packages_providing.add(package)
        #logger.debug('Packages providing %s: %s', requirement, packages_providing)
        return packages_providing
        
    def is_requirement_installed(self, requirement):
        """ This function does something.

            :param name: The name to use.
            :type name: str.
            :param state: Current state to be in.
            :type state: bool.
            :returns:  int -- the return code.
            :raises: AttributeError, KeyError
        """
        return len(self.find_packages_providing(requirement)) > 0
        
    def get_rpm_header(self, filename):
        """ Read an rpm header. """
        fd = os.open(filename, os.O_RDONLY)
        try:
            h = self.get_ts().hdrFromFdno(fd)
        finally:
            os.close(fd)
        return h    
        
        
    def check(self, cb=None):
        if cb is None:
            cb = self._check_callback
        return self.get_ts().check(cb)
    
    def _check_callback(self, ts, TagN, N, EVR, Flags):
        #if TagN == rpm.RPMTAG_REQUIRENAME:
        #    prev = ""
        #    
        #Nh = None
        #
        #if N[0] == '/':
        #    dbitag = 'basenames'
        #else:
        #    dbitag = 'providename'
        #
        ## What do you need to do.
        #if EVR:
        #    print "Must find package [", N, "-", EVR, "]"
        #else:
        #    print "Must find file [", N, "]"
        #
        #if resolved:
        #    # ts.addIntall(h, h, 'i')
        #    return -1
        pass
            
    _script_output_file = None
    _script_output_fd = None
    
    def enable_logging(self, rpmverbosity="info"):
        # set up the transaction to record output from scriptlets
        self._script_output_file = tempfile.NamedTemporaryFile()
        #self._readpipe = io_r
        self._script_output_fd = open(self._script_output_file.name, 'w+b')
        #rpm.setScriptFd(self._script_output_fd)
        
        if rpmverbosity not in self.LOG_LEVELS:
            raise ValueError('rpmverbosity not one of %s' % self.LOG_LEVELS.keys())
        
        rpm.setVerbosity(self.LOG_LEVELS.get(rpmverbosity))
        rpm.setLogFile(self._script_output_fd)
        
        logging.debug('Logging rpm to %s', self._script_output_file.name)
     
    def set_debug(self, enabled):
        if enabled:
            self.get_ts().Debug(1)
        else:
            self.get_ts().Debug(0)
            
    def get_problems(self):
        return self.get_ts().problems()
        
    def disable_logging(self):
        # reset rpm bits from reording output
        rpm.setVerbosity(rpm.RPMLOG_NOTICE)
        rpm.setLogFile(sys.stderr)
        try:
            self._script_output_fd.close()
        except Exception:
            pass
        
    def find_packages_requiring(self, r):
        # do a fast name check
        mi = self.get_ts().dbMatch(rpm.RPMTAG_REQUIRES, r.get_name())
        logger.debug('Checking local packages requiring %s %s', r, len(mi))
        matches = set()
        # check that the packages require r
        for h in mi:
            rpmheader = RPMInstalledPackage(h)
            #if rpmheader.requires(r):
            matches.add(rpmheader)
        return matches
        
    # RPMTAG_COLLECTIONS, RPMTAG_CONFLICTNAME, RPMTAG_DEPENDSDICT, RPMTAG_FILENAMES, RPMTAG_FILEPROVIDE
    # RPMTAG_FILERDEVS  RPMTAG_FILEREQUIRE, RPMTAG_INSTALLTID, RPMTAG_INSTPREFIXES, RPMTAG_INSTPREFIXES, RPMTAG_NAME
    # RPMTAG_NEVR, RPMTAG_NEVRA, RPMTAG_NVR, RPMTAG_NVRA, RPMTAG_PREFIXES, RPMTAG_PROVIDENAME, RPMTAG_PROVIDES
    def find_packages_providing(self, requirement):
        # search several places for the provide
        providename = requirement.get_name()
        transaction = self.get_ts()
        logger.log(5, 'Checking local basenames for %s', providename)
        mi = transaction.dbMatch(rpm.RPMTAG_BASENAMES, providename)
        
        matches = set()
        logger.log(5, 'Got %s matches', len(mi))
        for h in mi:
            rpmheader = RPMInstalledPackage(h)
            if rpmheader.meets_requirement(requirement):
                matches.add(rpmheader)
                continue
            else:
                logger.debug('Match %s doesnt meet requirement %s', rpmheader, requirement)
         
        # not in basenames, try names
        logger.log(5, 'Checking local names for %s', providename)
        mi = transaction.dbMatch(rpm.RPMTAG_NAME, providename)
        logger.log(5, 'Got %s matches', len(mi))
        for h in mi:
            rpmheader = RPMInstalledPackage(h)
            if rpmheader.meets_requirement(requirement):
                matches.add(rpmheader)
            else:
                logger.debug('Match %s doesnt meet requirement %s', rpmheader, requirement)
            
        logger.log(5, 'Checking local provides for %s', providename)
        mi = transaction.dbMatch(rpm.RPMTAG_PROVIDENAME, providename)
        logger.log(5, 'Got %s matches', len(mi))
        for h in mi:
            rpmheader = RPMInstalledPackage(h)
            if rpmheader.meets_requirement(requirement):
                matches.add(rpmheader)
            else:
                logger.debug('Match %s doesnt meet requirement %s', rpmheader, requirement)
        
        
        logger.log(5, 'Found %s packages providing %s', len(matches), requirement)
        return matches
        
    def glob_name(self, nameglob):
        mi = self.get_ts().dbMatch()
        mi.pattern('name', rpm.RPMMIRE_GLOB, nameglob)
    
    def order(self):
        return self.get_ts().order()
        
    def run(self, cb=None):
        """
            Returns:
                
                None if the transaction went ok.
                A list of errors if the transaction failed.
        """
        if cb is None:
            cb = self._run_callback
        return self.get_ts().run(cb, None)
        
    def _run_callback(self, reason, amount, total, pkg_filename, client_data=None):
        #logger.debug('Run callback: %s %s %s %s %s',reason, amount, total, key, client_data )
        cb_table = {
            rpm.RPMCALLBACK_INST_OPEN_FILE: self._run_callback_open_file,
            rpm.RPMCALLBACK_INST_CLOSE_FILE: self._run_callback_close_file,
            rpm.RPMCALLBACK_INST_START: self._run_callback_inst_start,
            rpm.RPMCALLBACK_TRANS_PROGRESS: self._run_callback_trans_progress,
            rpm.RPMCALLBACK_INST_PROGRESS: self._run_callback_inst_progress,
            rpm.RPMCALLBACK_TRANS_START: self._run_callback_trans_start,
            rpm.RPMCALLBACK_TRANS_STOP: self._run_callback_trans_stop,
            rpm.RPMCALLBACK_UNINST_START: self._run_callback_uninst_start,
            rpm.RPMCALLBACK_UNINST_STOP: self._run_callback_uninst_stop,
        }
        
        if reason not in cb_table:
            raise Exception("Unknown callback reason %s" % reason)
        
        return cb_table[reason](amount, total, pkg_filename, client_data)
      
    _fd  = None      
    def _run_callback_open_file(self, amount, total, pkg_filename, client_data):
        pkg, path = pkg_filename
        #pkgstr = str(info.getPackage())
        logger.log(5, "Opening %s...", pkg)
        #self.topic = _("Output from %s:") % pkgstr
        self._fd = os.open(path, os.O_RDONLY)
        #setCloseOnExec(self.fd)
        return self._fd
        
    def _run_callback_close_file(self, amount, total, key, client_data):
        if self._fd is not None:
            os.close(self._fd)
            self._fd = None

    def _run_callback_inst_start(self, amount, total, pkg_filename, client_data):
        #info, path = infopath
        #pkg = info.getPackage()
        #self.data["item-number"] += 1
        #self.prog.add(1)
        #self.prog.setSubTopic(infopath, _("Installing %s") % pkg.name)
        #self.prog.setSub(infopath, 0, 1, subdata=self.data)
        #self.prog.show()
        pkg, path = pkg_filename
        logger.info('Installing %s...', pkg)
        self.call_progress_callback('inst_start', amount, total, pkg_filename, client_data)
    def _run_callback_trans_progress(self, amount, total, pkg_filename, client_data):
        #pkg, path = pkg_filename
        logger.debug('Trans progress %s amt=%s, total=%s', pkg_filename, amount, total)
        #self.prog.setSub(infopath or "trans", amount, total,
        #                 subdata=self.data)
        #self.prog.show()
    def _run_callback_inst_progress(self, amount, total, pkg_filename, client_data):
        pkg, path = pkg_filename
        logger.log(5, 'Install progress %s amt=%s, total=%s', pkg, amount, total)
        self.call_progress_callback('inst_progress', amount, total, pkg_filename, client_data)
        #self.prog.setSub(infopath or "trans", amount, total,
        #                 subdata=self.data)
        #self.prog.show()
    
    def _run_callback_trans_start(self, amount, total, pkg_filename, client_data):
        #pkg, path = pkg_filename
        logger.debug('Trans start %s amt=%s, total=%s', pkg_filename, amount, total)
        #self.prog.setSubTopic("trans", _("Preparing..."))
        #self.prog.setSub("trans", 0, 1)
        #self.prog.show()
    
    def _run_callback_trans_stop(self, amount, total, pkg_filename, client_data):
        #pkg, path = pkg_filename
        logger.debug('Trans stop %s amt=%s, total=%s', pkg_filename, amount, total)
        #self.prog.setSubDone("trans")
        #self.prog.show()
    
    def _run_callback_uninst_start(self, amount, total, pkg_filename, client_data):
        #pkg, path = pkg_filename
        #logger.debug('Uninstall start %s amt=%s, total=%s', pkg_filename, amount, total)
        logger.info('Uninstalling %s...' % pkg_filename)
        #self.topic = _("Output from %s:") % infopath
        #subkey =  "R*"+infopath
        #self.data["item-number"] += 1
        #self.prog.add(1)
        #if infopath in self.upgradednames:
        #    topic = _("Cleaning %s") % infopath
        #else:
        #    topic = _("Removing %s") % infopath
        #self.prog.setSubTopic(subkey, topic)
        #self.prog.setSub(subkey, 0, 1, subdata=self.data)
        #self.prog.show()
    
    def _run_callback_uninst_stop(self, amount, total, pkg_filename, client_data):
        #pkg, path = pkg_filename
        logger.debug('Uninstall stop %s amt=%s, total=%s', pkg_filename, amount, total)
        #self.topic = None
        #subkey = "R*"+infopath
        #if not self.prog.getSub(subkey):
        #    self.data["item-number"] += 1
        #    self.prog.add(1)
        #    if infopath in self.upgradednames:
        #        topic = _("Cleaning %s") % infopath
        #    else:
        #        topic = _("Removing %s") % infopath
        #    self.prog.setSubTopic(subkey, topic)
        #    self.prog.setSub(subkey, 1, 1, subdata=self.data)
        #else:
        #    self.prog.setSubDone(subkey)
        #self.prog.show()
        
    def initdb(self):
        """ The transaction set provides an initDB method to initialize a new
            RPM database. This acts like the rpm --initdb command.
        """
        self.get_ts().initDB()
    
    def rebuilddb(self):
        """ The rebuildDB method regenerates the RPM database indices, like the
            rpm --rebuilddb command.
        """
        self.get_ts().rebuildDB()
        
    def verifydb(self):
        """ The verifyDB method checks that the RPM database and indices are
            readable by the Berkeley DB library.
            
            Calling this method is the same as running the db_verify command on
            each of the database files in /var/lib/rpm.
        """
        self.get_ts().verifyDB()
        
    
    
        
    def test(self, cb):
        """tests the ts we've setup, takes a callback function"""
    
        origflags = self.get_flags()
        self.addTsFlag(rpm.RPMTRANS_FLAG_TEST)
        self.get_ts().setProbFilter(rpm.RPMPROB_FILTER_DISKSPACE)
        tserrors = self.get_ts().run(cb.callback, '')
        self.get_ts().setFlags(origflags)
    
        reserrors = []
        if tserrors:
            for (descr, (etype, mount, need)) in tserrors:
                reserrors.append(descr)
        
        return reserrors
        
        
